Skip to main content

03 快属性和慢属性:提升对象属性的访问速度

JavaScript 中的对象是由一组组属性和值的集合,从 JavaScript 语言的角度来看,JavaScript 对象像一个字典,字符串作为键名,任意对象可以作为键值,可以通过键名读写键值。

在 V8 实现对象存储时,出于性能的考量,并没有完全采用字典的存储方式,因为字典是非线性的数据结构,查询效率会低于线性的数据结构。

常规属性 (properties) 和排序属性 (element)

function Foo() {
this[100] = 'test-100'
this[1] = 'test-1'
this["B"] = 'bar-B'
this[50] = 'test-50'
this["A"] = 'bar-A'
this["C"] = 'bar-C'
}
var bar = new Foo()


for(key in bar){
console.log(`index:${key} value:${bar[key]}`)
}

// result
index:1 value:test-1
index:50 value:test-50
index:100 value:test-100
index:B value:bar-B
index:A value:bar-A
index:C value:bar-C
  • 数字属性最先打印出来,并且按照数字大小的顺序;
  • 字符串属性按照设置顺序打印。

**在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。**把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性被称为常规属性,在 V8 中被称为 properties。

在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性:

bar 对象包含了两个隐藏属性:elements 属性和 properties 属性,elements 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性,properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存了常规属性。

快属性和慢属性

在 V8 中查找 properties 属性中的 B 元素,需要先查找 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种方式在查找过程中增加了一步操作,因此会影响到元素的查找效率。

因此 V8 采取了一个权衡的策略以加快查找属性的效率,将部分常规属性直接存储到对象本身,称为对象内属性 (in-object properties):

对象内属性的数量是固定的,默认是 10 个,如果添加的属性超出了对象分配的空间,则它们将被保存在常规属性存储中。

将保存在线性数据结构中的属性称之为“快属性”,线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。

因此如果一个对象的属性过多时,V8 就会采取另外一种存储策略,慢属性策略,但慢属性的对象内部会有独立的非线性数据结构 (词典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。

在 Chrome 中查看对象布局

结合 Chrome 中的内存快照,查看对象在内存中是如何布局的。在控制台中执行以下代码查看内存快照:

function Foo(property_num, element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`;
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`;
this[ppt] = ppt;
}
}
var bar = new Foo(10, 10);

将 Chrome 开发者工具切换到 Memory 标签,然后点击左侧的小圆圈就可以捕获当前的内存快照:

在搜索框里面输入构造函数 Foo,Chrome 会列出所有经过构造函数 Foo 创建的对象:

  • 10 个常规属性作为对象内属性,存放在 bar 函数内部;
  • 10 个排序属性存放在 elements 中。

var bar2 = new Foo(20,10)

  • 10 属性直接存放在 bar2 的对象内 ;
  • 10 个常规属性以线性数据结构的方式存放在 properties 属性里面 ;
  • 10 个数字属性存放在 elements 属性里面。

var bar3 = new Foo(100,10)

  • 10 属性直接存放在 bar3 的对象内 ;
  • 90 个常规属性以非线性字典的这种数据结构方式存放在 properties 属性里面 ;
  • 10 个数字属性存放在 elements 属性里面。

其他属性

除了 elements 和 properties 属性,V8 还为每个对象实现了 map 属性和 proto 属性。

  • proto 属性就是原型,用来实现 JavaScript 继承;
  • map 是隐藏类,用来在内存中快速查找对象属性。